/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.andware.volley.toolbox;

import android.util.Log;

import com.andware.volley.AuthFailureError;
import com.andware.volley.Request;
import com.andware.volley.Request.Method;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

/**
 * An {@link HttpStack} based on {@link HttpURLConnection}.
 */
public class HurlStack implements HttpStack {

    private static final String HEADER_CONTENT_TYPE = "Content-Type";

    private final static String HEADER_TOKEN = "token";

    private final static String HEADER_COOKIE = "Cookie";

    /**
     * An interface for transforming URLs before use.
     */
    public interface UrlRewriter {
        /**
         * Returns a URL to use instead of the provided one, or null to indicate
         * this URL should not be used at all.
         */
        public String rewriteUrl ( String originalUrl );
    }

    private final UrlRewriter mUrlRewriter;
    private final SSLSocketFactory mSslSocketFactory;

    public HurlStack () {
        this ( null );
    }

    /**
     * @param urlRewriter Rewriter to use for request URLs
     */
    public HurlStack ( UrlRewriter urlRewriter ) {
        this ( urlRewriter, null );
    }

    /**
     * @param urlRewriter      Rewriter to use for request URLs
     * @param sslSocketFactory SSL factory to use for HTTPS connections
     */
    public HurlStack ( UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory ) {
        mUrlRewriter = urlRewriter;
        mSslSocketFactory = sslSocketFactory;
    }

    @Override
    public HttpResponse performRequest ( Request< ? > request, Map< String, String > additionalHeaders )
            throws IOException, AuthFailureError {
        String url = request.getUrl ();
        HashMap< String, String > map = new HashMap< String, String > ();
        map.putAll ( request.getHeaders () );
        map.putAll ( additionalHeaders );
        Log.i ( "Token", "token:" + request.getToken () );
        if ( request.getToken () != null ) {
            map.put ( HEADER_TOKEN, request.getToken () );
        }
        Log.i ( "Cookie", "cookie:" + request.getCookies () );
        if ( request.getCookies () != null ) {
            map.put ( HEADER_COOKIE, request.getCookies () );
        }
        if ( mUrlRewriter != null ) {
            String rewritten = mUrlRewriter.rewriteUrl ( url );
            if ( rewritten == null ) {
                throw new IOException ( "URL blocked by rewriter: " + url );
            }
            url = rewritten;
        }
        URL parsedUrl = new URL ( url );
        HttpURLConnection connection = openConnection ( parsedUrl, request );

        for ( String headerName : map.keySet () ) {
            connection.addRequestProperty ( headerName, map.get ( headerName ) );
        }
        setConnectionParametersForRequest ( connection, request );
        // Initialize HttpResponse with data from the HttpURLConnection.
        ProtocolVersion protocolVersion = new ProtocolVersion ( "HTTP", 1, 1 );
        int responseCode = connection.getResponseCode ();
        if ( responseCode == - 1 ) {
            // -1 is returned by getResponseCode() if the response code could not be retrieved.
            // Signal to the caller that something was wrong with the connection.
            throw new IOException ( "Could not retrieve response code from HttpUrlConnection." );
        }
        StatusLine responseStatus = new BasicStatusLine ( protocolVersion,
                connection.getResponseCode (), connection.getResponseMessage () );
        BasicHttpResponse response = new BasicHttpResponse ( responseStatus );
        response.setEntity ( entityFromConnection ( connection ) );
        for ( Entry< String, List< String > > header : connection.getHeaderFields ().entrySet () ) {
            if ( header.getKey () != null ) {
                Header h = new BasicHeader ( header.getKey (), header.getValue ().get ( 0 ) );
                response.addHeader ( h );
            }
        }
        return response;
    }

    /**
     * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
     *
     * @param connection
     * @return an HttpEntity populated with data from <code>connection</code>.
     */
    private static HttpEntity entityFromConnection ( HttpURLConnection connection ) {
        BasicHttpEntity entity = new BasicHttpEntity ();
        InputStream inputStream;
        try {
            inputStream = connection.getInputStream ();
        } catch ( IOException ioe ) {
            inputStream = connection.getErrorStream ();
        }
        entity.setContent ( inputStream );
        entity.setContentLength ( connection.getContentLength () );
        entity.setContentEncoding ( connection.getContentEncoding () );
        entity.setContentType ( connection.getContentType () );
        return entity;
    }

    /**
     * Create an {@link HttpURLConnection} for the specified {@code url}.
     */
    protected HttpURLConnection createConnection ( URL url ) throws IOException {
        if ( url.toString ().contains ( "https" ) ) {
            HTTPSTrustManager.allowAllSSL ();
        }
        return ( HttpURLConnection ) url.openConnection ();
    }

    /**
     * Opens an {@link HttpURLConnection} with parameters.
     *
     * @param url
     * @return an open connection
     * @throws IOException
     */
    private HttpURLConnection openConnection ( URL url, Request< ? > request ) throws IOException {
        HttpURLConnection connection = createConnection ( url );

        int timeoutMs = request.getTimeoutMs ();
        connection.setConnectTimeout ( timeoutMs );
        connection.setReadTimeout ( timeoutMs );
        connection.setUseCaches ( false );
        connection.setDoInput ( true );

        // use caller-provided custom SslSocketFactory, if any, for HTTPS
        if ( "https".equals ( url.getProtocol () ) && mSslSocketFactory != null ) {
            ( ( HttpsURLConnection ) connection ).setSSLSocketFactory ( mSslSocketFactory );
        }

        return connection;
    }

    @SuppressWarnings ( "deprecation" )
    /* package */ static void setConnectionParametersForRequest ( HttpURLConnection connection,
                                                                  Request< ? > request ) throws IOException, AuthFailureError {
        switch ( request.getMethod () ) {
            case Method.DEPRECATED_GET_OR_POST:
                // This is the deprecated way that needs to be handled for backwards compatibility.
                // If the request's post body is null, then the assumption is that the request is
                // GET.  Otherwise, it is assumed that the request is a POST.
                byte[] postBody = request.getPostBody ();
                if ( postBody != null ) {
                    // Prepare output. There is no need to set Content-Length explicitly,
                    // since this is handled by HttpURLConnection using the size of the prepared
                    // output stream.
                    connection.setDoOutput ( true );
                    connection.setRequestMethod ( "POST" );
                    connection.addRequestProperty ( HEADER_CONTENT_TYPE,
                            request.getPostBodyContentType () );
                    DataOutputStream out = new DataOutputStream ( connection.getOutputStream () );
                    out.write ( postBody );
                    out.close ();
                }
                break;
            case Method.GET:
                // Not necessary to set the request method because connection defaults to GET but
                // being explicit here.
                connection.setRequestMethod ( "GET" );
                break;
            case Method.DELETE:
                connection.setRequestMethod ( "DELETE" );
                break;
            case Method.POST:
                connection.setRequestMethod ( "POST" );
                addBodyIfExists ( connection, request );
                break;
            case Method.PUT:
                connection.setRequestMethod ( "PUT" );
                addBodyIfExists ( connection, request );
                break;
            case Method.HEAD:
                connection.setRequestMethod ( "HEAD" );
                break;
            case Method.OPTIONS:
                connection.setRequestMethod ( "OPTIONS" );
                break;
            case Method.TRACE:
                connection.setRequestMethod ( "TRACE" );
                break;
            case Method.PATCH:
                connection.setRequestMethod ( "PATCH" );
                addBodyIfExists ( connection, request );
                break;
            default:
                throw new IllegalStateException ( "Unknown method type." );
        }
    }

    private static void addBodyIfExists ( HttpURLConnection connection, Request< ? > request )
            throws IOException, AuthFailureError {
        byte[] body = request.getBody ();
        if ( body != null ) {
            connection.setDoOutput ( true );
            connection.addRequestProperty ( HEADER_CONTENT_TYPE, request.getBodyContentType () );
            DataOutputStream out = new DataOutputStream ( connection.getOutputStream () );
            out.write ( body );
            out.close ();
        }
    }
}
